////////////////////////////////////////////////////////////////////////////////
//
//  based on Adobe source
//
//  link:
//
//  http://opensource.adobe.com/svn/opensource/flex/sdk/tags/4.0.0.4021/frameworks/projects/flex4/src/mx/graphics/Parser.as
//
// enhancement by suspendmode@gmail.com
//
//
////////////////////////////////////////////////////////////////////////////////

package com.google.code.fxg
{
	
	import flash.display.Bitmap;
	import flash.display.Loader;
	import flash.display.LoaderInfo;
	import flash.events.Event;
	import flash.net.URLRequest;
	import flash.utils.Dictionary;
	
	import flashx.textLayout.conversion.TextConverter;
	import flashx.textLayout.elements.TextFlow;
	
	import com.google.code.fxg.schema.ArrayReference;
	import com.google.code.fxg.schema.FXGSchema;
	import com.google.code.fxg.schema.ObjectElement;
	import com.google.code.fxg.schema.ObjectElementGroup;
	
	import spark.components.RichText;
	import spark.primitives.*;
	import com.google.code.fxg.namespace.mxmlg;
	import com.google.code.fxg.namespace.unbound;
			
	use namespace mxmlg;
	use namespace unbound;
	
	
	/**	 
	 *  This class can parse XML containing FXG
	 *  and create an instance of the Graphics class to display it.
	 */
	public class FXGDecoder
	{		
		
		//-----------------------------------------------------------------------------
		//
		//  Class methods
		//
		//-----------------------------------------------------------------------------
		
		/**
		 *  @private
		 */
		public static function skipElement(decoder:FXGDecoder, parentInstance:*,
												  node:XML):void     
		{
		}
		
		/**
		 *  @private
		 */
		public static function fromXMLLibrary(decoder:FXGDecoder, parentInstance:*,
												   node:XML):void
		{
			var children:XMLList = node.children();
			var n:int = children.length();
			for (var i:int = 0; i < n; i++)
			{
				var child:XML = children[i];
				if (child.namespace() != mxmlg || child.localName() != "Definition")
				{
					decoder.reportError("Library Elements can only contain Definition elements");
					continue;
				}
				fromXMLDefinition(decoder,child);
			}
		}
		
		/**
		 *  Method to parse <content> tags inside of <TextGraphic>.
		 *  Should basically just send it to the new text engine, which knows what to do.
		 */
		public static function fromXMLTextGraphicTag(decoder:FXGDecoder,
														  parentInstance:*,
														  node:XML):void
		{
			// 'parentInstance' will be a TextGraphic instance.
			// 'node' will be a <content> node inside a <TextGraphic> node.
			
			// Have the Text Component Library parse the <TextGraphic> node
			// and return a TextFlow instance which we can set as the
			// 'content' of the TextGraphic instance.
							
			var flow:TextFlow = TextConverter.importToFlow(node.parent(), TextConverter.TEXT_LAYOUT_FORMAT);
			RichText(parentInstance).content = flow;
			
			// need to get errors from the new text engine
		}
		
		/**
		 *  @private
		 */
		private static function fromXMLDefinition(decoder:FXGDecoder, node:XML):void
		{
			var name:String = node.@name;
			
			if (name == "")
			{
				decoder.reportError("definitions must have a valid name");
				return;
			}
			
			if (node.length() != 1)
			{
				decoder.reportError("definitions must have exactly one child element of type Group");
				return;
			} 
			
			var child:XML = node.children()[0];
			if (child.namespace() != mxmlg || child.localName() != "Group")
			{
				decoder.reportError("definitions must have exactly one child element of type Group");
				return;
			} 
			
			decoder.definitions[name] = child;           
		}
		
		/**
		 *  @private
		 */
		private static function fromXMLUnknownElement(decoder:FXGDecoder,
													desc:ObjectElementGroup,
													node:XML):*
		{
			var name:String = node.localName();
			
			if (name in decoder.definitions)
			{
				var instance:* = fromXMLObjectElement(decoder, FXGSchema.groupDefinitionType,
					decoder.definitions[name]);
				if (instance != undefined)
					applyObjectElementToInstance(decoder,FXGSchema.placeObjectType,node,instance);
				
				return instance;
			}
			else
			{
				decoder.reportError("Unexpected Element of Type " + name + " found.");
				
				return undefined;   
			}
		}
		
		/**
		 *  @private
		 */
		private static function fromXMLObjectElementGroup(decoder:FXGDecoder,
														desc:ObjectElementGroup,
														node:XML):*               
		{           
			var name:String = node.localName();
			
			var handler:* = desc.types[name];
			
			if (handler == null)
				return fromXMLUnknownElement(decoder,desc,node);
			
			if (handler is ObjectElement)
			{
				return fromXMLObjectElement(decoder,handler,node);
			}
			else
			{
				decoder.reportError("Unexpected handler type found for element of type " + name + ".");
				return undefined;
			}
		}
		
		/**
		 *  @private
		 */
		private static function fromXMLPropertyReference(decoder:FXGDecoder, handler:*,
													   node:XML):*
		{
			if (handler is ArrayReference)
			{
				var result:Array = [];
				
				var children:XMLList = node.children();
				var n:int = children.length();
				for (var i:int = 0; i < n; i++)
				{
					var child:XML = children[i];
					
					var newValue:* = fromXMLOneNode(
						decoder, (handler as ArrayReference).handler, child);
					
					if (newValue != undefined)
						result.push(newValue);
				}
				
				return result;
			}
			else
			{
				children = node.children();
				if (children.length() != 1)
				{
					decoder.reportError("Property element of type " + node.localName() + " must have exactly one child element");
					return undefined;
				}
				
				return fromXMLOneNode(decoder, handler, children[0]);
			}
		}
		
		/**
		 *  @private
		 */
		private static function fromXMLOneNode(decoder:FXGDecoder, handler:*, node:XML):*
		{
			if (node.namespace() != mxmlg )
			{
				decoder.reportError("illegal foreign element " + node.name() + " found in FXG structure");
				return undefined;
			}
			
			var name:String = node.localName();
			
			if (handler is ObjectElement)
			{
				return fromXMLObjectElement(decoder,handler,node);
			}
			else if (handler is ObjectElementGroup)
			{
				return fromXMLObjectElementGroup(decoder,handler,node);
			}
			else
			{
				decoder.reportError("Unexpected handler type found for element of type " + name + ".");
				return undefined;
			}
		}
		
		/**
		 *  @private
		 */
		private static function fromXMLObjectElement(decoder:FXGDecoder,
												   desc:ObjectElement,
												   node:XML):*             
		{           
			if (desc.name != node.localName())
			{
				decoder.reportError("Unexpected Element of Type " + node.localName() + " found.");
				return undefined;               
			}
			
			// Create instance for this element.
			var result:* = new desc.type();
			
			applyObjectElementToInstance(decoder, desc, node, result);
			
			return result;
		}
		
		/**
		 *  @private
		 */
		private static function applyObjectElementToInstance(decoder:FXGDecoder,
															 desc:ObjectElement,
															 node:XML,
															 instance:*):void 
		{
			// Apply attributes to instance.
			var atts:XMLList = node.attributes();
			var n:int = atts.length();
			for (var i:int = 0; i < n; i++)
			{
				var attribute:XML = atts[i];
				
				var ns:Namespace = attribute.namespace();
				if (ns != mxmlg && ns != unbound)
					continue;                   
				
				fromXMLObjectAttribute(decoder, instance, attribute, desc);
			}
			
			// Apply children to instance.
			var defaultValues:Array = [];
			var children:XMLList = node.children();
			n = children.length();
			for (i = 0; i < n; i++)
			{
				var child:XML = children[i];
				
				var name:String;
				var handler:*;
				
				if (child.nodeKind() == "content")
				{
					handler = null;
				}
				else if (child.namespace() != mxmlg)
				{
					decoder.reportError("illegal foreign element " + child.name() + " found in FXG structure");
					continue;
				}
				else
				{
					name = child.localName();
					handler = desc.elements[name];
				}               
				
				var newValue:*;
				
				if (handler is ObjectElement ||
					handler is ObjectElementGroup ||
					handler is ArrayReference)
				{
					newValue = fromXMLPropertyReference(decoder, handler, child);
					
					if (newValue != undefined)
					{
						if (name in instance)
						{                      
							instance[name] = newValue;
						}
						else
						{
							decoder.reportError("child element " + name + " not implemented on element of type " + desc.type);
							continue;
						}
					}
				}
				else if (handler is Function)
				{
					handler(decoder, instance, child);
				}
				else if (handler == null)
				{
					if (desc.defaultTypes != null)
					{
						newValue = fromXMLObjectElementGroup(decoder, desc. defaultTypes, child);
						if (newValue != undefined)
							defaultValues.push(newValue);
					}
					else if (child.nodeKind() == "content")
					{
						defaultValues.push(child.toString());
					}
					else
					{
						decoder.reportError("Unexpected Element of Type " + name + " found.");
						continue;
					}
				}
				else
				{
					decoder.reportError("Unexpected handler type found for element of type " + name + ".");
					continue;
				}
			}
			
			if (defaultValues.length > 0 && desc.defaultProperty != null)
			{
				if (desc.defaultTypes == null)
				{
					// text property
					instance[desc.defaultProperty] = defaultValues.join();
				}
				else
				{
					// object typed
					instance[desc.defaultProperty] = defaultValues;
				}
			}
		}
		
		/**
		 *  @private
		 */
		private static function fromXMLObjectAttribute(decoder:FXGDecoder,
													 instance:Object,
													 attribute:XML,
													 desc:ObjectElement):void
		{
			var legalAttributes:Object = desc.attributes;
			var name:String = attribute.localName();
			var value:*;
			
			switch (name)
			{
				case "id":
				{
					return;
				}            
			}
			
			if (!(name in legalAttributes))         
			{
				decoder.reportError("attribute " + name + " not allowed here");
				return;
			}
			
			switch (legalAttributes[name])
			{
				case FXGSchema.NUMBER:
				{
					value = parseFloat(attribute.toString());
					break;
				}
					
				case FXGSchema.NUMBERLIST:
				{
					var values:Array = attribute.toString().split(",");
					value = [];
					var n:int = values.length;
					for (var i:int = 0; i < n; i++)
						value[i] = parseFloat(values[i]);
					break;
				}
					
				case FXGSchema.COLOR:
				{
					value = parseInt(attribute.toString().replace("#", "0x"));
					break;
				}
					
				case FXGSchema.BOOLEAN:
				{
					value = attribute.toString() == "true" ? true : false;
					break;
				}
					
				case FXGSchema.STRING:
				case FXGSchema.IDENTIFIER:
				{
					value = attribute.toString();
					break;
				}
					
				case FXGSchema.IGNORE:
				{
					break;
				}
					
				case FXGSchema.EMBED:
				{
					var match:Array = attribute.toString().match(/\w*@Embed\(\w*[\"\'](.*)[\"\']\w*\)\w*/); 
					if (match == null || match.length != 2)
					{
						decoder.reportError("Malformed Embed Attribute: " + attribute.toString());
						return;
					}
					
					var url:String;
					if (decoder.urlResolver != null)
						url = decoder.urlResolver(match[1]);
					else
						url = match[1];
					//pull the '/' out of the url since Thermo auto-gens this  
					if (url.charAt(0) == "/")
						url = url.slice(1);
					
					var loader:Loader = new Loader();
					loader.load(new URLRequest(url));
					loader.contentLoaderInfo.addEventListener(
						Event.COMPLETE, decoder.embedCompleteHandler);
					decoder.embeds[loader] =
						{ loader: loader, property: name, instance: instance };
					break;
				}
					
				default:
				{
					decoder.reportError("unknown attribute type " + legalAttributes[name] + " discovered for attribute " + name);
					return;
				}
			}
			
			if (value == undefined)
				return;
			
			if (name in instance)
				instance[name] = value;
			else
				decoder.reportError("attribute " + name + " not implemented on elements of type " + desc.name + " yet.");
		}
		
		//-----------------------------------------------------------------------------
		//
		//  Constructor
		//
		//-----------------------------------------------------------------------------
		
		/**
		 *  Constructor.
		 */
		public function FXGDecoder()
		{
			super();
		}
		
		//-----------------------------------------------------------------------------
		//
		//  Variables
		//
		//-----------------------------------------------------------------------------
		
		/**
		 *  @private
		 */
		private var embeds:Dictionary = new Dictionary(false);
		
		/**
		 *  @private
		 */
		private var definitions:Object = {};
		
		/**
		 *  @private
		 */
		public var urlResolver:Function;
		
		/**
		 *  @private
		 */
		public var errors:Array = [];
		
		/**
		 *  @private
		 */
		public var throwOnError:Boolean = false;
		
		//-----------------------------------------------------------------------------
		//
		//  Methods
		//
		//-----------------------------------------------------------------------------
		
		/**
		 *  Parses <Graphic> XML to a Graphic instance.
		 */
		public function fromXML(source:XML):Graphic
		{
			definitions = {};
			embeds = new Dictionary(false);
			errors = [];            
			
			return fromXMLObjectElement(this, FXGSchema.graphicType, source);
		}
		
		/**
		 *  @private
		 */
		private function reportError(error:String):void
		{
			errors.push(error);
			
			if (throwOnError)
				throw new Error(error);
		}
		
		//-----------------------------------------------------------------------------
		//
		//  Event handlers
		//
		//-----------------------------------------------------------------------------
		
		/**
		 *  @private
		 */
		private function embedCompleteHandler(e:Event):void
		{
			var loaderInfo:LoaderInfo = LoaderInfo(e.currentTarget);
			var embedData:Object = embeds[loaderInfo.loader];
			embedData.instance[embedData.property] =
				Bitmap(loaderInfo.content).bitmapData;
		}
	}
}